Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610 yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。

- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
  DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s) Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Frontend as useResumeDraftPdf
  participant Router as agent router
  participant Cache as resume_draft_cache
  participant Worker as run_resume_draft_task
  participant LLM as LLM/PDF builder
  participant Billing as credit_service

  Frontend->>Router: POST /resume-draft/run
  Router->>Router: ensure_can_use_model, build_draft_source
  Router->>Cache: get_or_create + set status=pending
  Router->>Worker: dispatch background task
  Router-->>Frontend: 202 TaskAcceptedResponse

  loop polling
    Frontend->>Router: GET /resume-draft/status
    Router->>Cache: read status
    Router-->>Frontend: TaskStatusResponse
  end

  Worker->>Cache: set status=processing
  Worker->>LLM: run_resume_draft (generate)
  alt LLM/parse failure
    Worker->>Billing: charge consumed tokens
    Worker->>Cache: raise NonRetryableError (dead_letter)
  else success
    Worker->>LLM: build_resume_pdf (validate)
    alt PDF failure
      Worker->>Cache: raise NonRetryableError (no charge)
    else PDF valid
      Worker->>Billing: record_chat_usage (final charge)
      Worker->>Cache: write result, status=completed
    end
  end

  Frontend->>Router: GET /resume-draft/pdf
  Router->>Cache: read result (completed)
  Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels: feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
def test_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) -> None:
    """既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""
    headers = auth_header(client, github_id=1)
    _seed_link_data(client._db_session)
    user = client._db_session.query(User).filter_by(username="testuser").one()
    client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
    client._db_session.commit()

    res = client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)

    assert res.status_code == 202
    assert res.json()["status"] == "processing"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
   const generate = async () => {
     if (generating) return;
     setGenerating(true);
     setError(null);
+    updatePreviewUrl(null);
     try {
       await startResumeDraft(model);
       startPolling();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。

In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.

---

Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.

In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.

In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread .claude/rules/backend/agent.md
Comment thread backend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into main Jul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent DevForge Agent backend バックエンド documentation Improvements or additions to documentation test テスト追加・修正 web フロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants